/**
 * Copyright (c) 2004-2006 Regents of the University of California.
 * See "license-prefuse.txt" for licensing terms.
 */
package core.proto.vis;

import core.proto.vis.action.FileOpenAction;
import core.proto.vis.action.SetLockNodeAction;
import core.proto.vis.action.ShowSearchResultEdgeAction;
import core.proto.vis.action.TreeRootAction;
import core.proto.vis.action.assignment.EdgeColourAction;
import core.proto.vis.action.assignment.TextColourAction;
import core.proto.vis.action.util.Position;
import core.proto.vis.controls.*;
import prefuse.Constants;
import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.ActionList;
import prefuse.action.ItemAction;
import prefuse.action.RepaintAction;
import prefuse.action.animate.ColorAnimator;
import prefuse.action.animate.PolarLocationAnimator;
import prefuse.action.animate.QualityControlAnimator;
import prefuse.action.animate.VisibilityAnimator;
import prefuse.action.assignment.DataColorAction;
import prefuse.action.assignment.DataSizeAction;
import prefuse.action.assignment.FontAction;
import prefuse.action.filter.GraphDistanceFilter;
import prefuse.action.layout.CollapsedSubtreeLayout;
import prefuse.action.layout.graph.RadialTreeLayout;
import prefuse.activity.SlowInSlowOutPacer;
import prefuse.controls.ControlAdapter;
import prefuse.controls.FocusControl;
import prefuse.controls.HoverActionControl;
import prefuse.controls.NeighborHighlightControl;
import prefuse.data.Graph;
import prefuse.data.Node;
import prefuse.data.Schema;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.io.GraphMLReader;
import prefuse.data.query.SearchQueryBinding;
import prefuse.data.search.PrefixSearchTupleSet;
import prefuse.data.search.SearchTupleSet;
import prefuse.data.tuple.DefaultTupleSet;
import prefuse.data.tuple.TupleSet;
import prefuse.render.*;
import prefuse.render.Renderer;
import prefuse.util.ColorLib;
import prefuse.util.FontLib;
import prefuse.util.GraphicsLib;
import prefuse.util.PrefuseLib;
import prefuse.util.display.DisplayLib;
import prefuse.util.ui.JFastLabel;
import prefuse.util.ui.JSearchPanel;
import prefuse.util.ui.UILib;
import prefuse.visual.EdgeItem;
import prefuse.visual.NodeItem;
import prefuse.visual.VisualItem;
import prefuse.visual.expression.InGroupPredicate;
import prefuse.visual.sort.TreeDepthItemSorter;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Visualization for CollabViz based on radial tree
 *
 * @author Bill
 */
public class VizDemoRadial extends Display {

    private static boolean STAND_ALONE = false;

    // public static final String DATA_FILE = "data/config/radial2.xml";

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    public static final String DATA_FILE = "data/config/similarity.xml";

    private static final String TREE = "tree";

    private static final String NODES = "tree.nodes";

    private static final String EDGES = "tree.edges";

    private static final DefaultListModel listModel = new DefaultListModel();

    public DefaultListModel getListModel() {
        return listModel;
    }

    // private static final String EDGE_DECORATORS = "edgeDeco";
    //
    // public static final String NODE_DECORATORS = "nodeDeco";

    private static final Schema DECORATOR_SCHEMA = PrefuseLib
            .getVisualItemSchema();

    static {
        DECORATOR_SCHEMA.setDefault( VisualItem.INTERACTIVE, false ); // noninteractive
        DECORATOR_SCHEMA.setDefault( VisualItem.TEXTCOLOR,
                ColorLib.rgba( 100, 100, 100, 200 ) );
        DECORATOR_SCHEMA.setDefault( VisualItem.FONT,
                FontLib.getFont( "Tahoma", 11 ) ); // and not too big
    }

    private static final String linear = "linear";

    public static final String BLUE_EDGE_SET = "_edgeset_";

    private LabelRenderer m_nodeRenderer;

    // ShapeRenderer m_nodeRenderer;

    private EdgeRenderer m_edgeRenderer;

    private String m_label = "label";

    private static Map<NodeItem, Position> nodePositionLock = new HashMap<NodeItem, Position>();

    private static Map<String, Integer> nameIdMapper = new HashMap<String, Integer>();

    private static Node getNodeByName( Graph g, String name ) {
        int nodeId = nameIdMapper.get( getUpperLetterOnly( name ) );
        return g.getNode( nodeId );
    }

    private Map<String, List<EdgeItem>> attrMapping = new HashMap<String, List<EdgeItem>>();

    private List<EdgeItem> prevHighlightedEdges = new ArrayList<EdgeItem>();

    public VizDemoRadial( final Graph g, final String label, final int width,
                          final int height ) {
        super( new Visualization() );

        createNameIdMapper( g );

        m_label = label;

        // -- set up visualization --
        m_vis.add( TREE, g );
        m_vis.setInteractive( EDGES, null, true );

        // -- set up renderers --

        m_nodeRenderer = new LabelRenderer( m_label );
        m_nodeRenderer.setRenderType( AbstractShapeRenderer.RENDER_TYPE_FILL );
        m_nodeRenderer.setHorizontalAlignment( Constants.CENTER );
        m_nodeRenderer.setRoundedCorner( 8, 8 );

        // m_nodeRenderer = new ShapeRenderer();

        m_edgeRenderer = new EdgeRenderer();

        DefaultRendererFactory rf = new DefaultRendererFactory( m_nodeRenderer );
        rf.add( new InGroupPredicate( EDGES ), m_edgeRenderer );

        Renderer polyR = new PolygonRenderer( Constants.POLY_TYPE_CURVE );
        ( ( PolygonRenderer ) polyR ).setCurveSlack( 0.15f );

        rf.add( "ingroup('aggregates')", polyR );
        // rf.add( new InGroupPredicate( EDGE_DECORATORS ), new LabelRenderer(
        // "name" ) );
        // rf.add( new InGroupPredicate( NODE_DECORATORS ), new LabelRenderer(
        // "name" ) );

        m_vis.setRendererFactory( rf );

        // add edge labels
        // DECORATOR_SCHEMA.setDefault( VisualItem.TEXTCOLOR, ColorLib.gray( 0 )
        // );

        // add the decorator
        // removed for now, does not display right beside the hovered item
        // m_vis.addDecorators( EDGE_DECORATORS, EDGES, new HoverPredicate(),
        // DECORATOR_SCHEMA );
        // m_vis.addDecorators( EDGE_DECORATORS, EDGES, DECORATOR_SCHEMA );
        // DECORATOR_SCHEMA.setDefault( VisualItem.TEXTCOLOR, ColorLib.gray( 0 )
        // );
        // m_vis.addDecorators( NODE_DECORATORS, NODES, DECORATOR_SCHEMA );
        // -- set up processing actions --

        // colors
        // ItemAction nodeColor = new NodeColorAction( NODES );
        // int[] palette = new int[] { ColorLib.rgb( 255, 180, 180 ),
        // ColorLib.rgb( 190, 190, 255 ) };

        int[] palette = new int[]{ ColorLib.rgb( 168, 211, 255 ),
                ColorLib.rgb( 11, 249, 249 ), ColorLib.rgb( 90, 210, 250 ),
                ColorLib.rgb( 201, 147, 255 ), ColorLib.rgb( 174, 94, 255 ),
                ColorLib.rgb( 224, 181, 251 ), ColorLib.rgb( 255, 176, 216 ),
                ColorLib.rgb( 249, 164, 106 ), ColorLib.rgb( 253, 123, 156 ),
                ColorLib.rgb( 251, 130, 226 ), ColorLib.rgb( 164, 235, 156 ) };

        DataColorAction nodeColor = new DataColorAction( NODES, "type",
                Constants.NOMINAL, VisualItem.FILLCOLOR, palette );

        ItemAction textColor = new TextColourAction( NODES );
        ItemAction edgeColor = new EdgeColourAction( EDGES );

        m_vis.putAction( "textColor", textColor );

        FontAction fonts = new FontAction( NODES, FontLib.getFont( "Tahoma",
                Font.BOLD, 10 ) );
        fonts.add( "ingroup('_focus_')",
                FontLib.getFont( "Tahoma", Font.BOLD, 15 ) );

        // recolor
        ActionList recolor = new ActionList();
        recolor.add( nodeColor );
        recolor.add( edgeColor );
        recolor.add( textColor );
        m_vis.putAction( "recolor", recolor );

        // repaint
        ActionList repaint = new ActionList();
        repaint.add( recolor );
        repaint.add( new RepaintAction() );
        m_vis.putAction( "repaint", repaint );

        // animate paint change
        ActionList animatePaint = new ActionList( 400 );
        animatePaint.add( new ColorAnimator( NODES ) );
        animatePaint.add( new RepaintAction() );
        m_vis.putAction( "animatePaint", animatePaint );

        // create the tree layout action
        RadialTreeLayout treeLayout = new RadialTreeLayout( TREE );
        treeLayout.setAutoScale( false );
        treeLayout.setRadiusIncrement( 125 );
        // treeLayout.setAngularBounds(-Math.PI/2, Math.PI);
        m_vis.putAction( "treeLayout", treeLayout );

        CollapsedSubtreeLayout subLayout = new CollapsedSubtreeLayout( TREE );
        m_vis.putAction( "subLayout", subLayout );

        DataSizeAction edgeSize = new DataSizeAction( EDGES, "weight" );

        // EdgeLabelLayout edgeLabel = new EdgeLabelLayout( EDGE_DECORATORS );

        // NodeLabelLayout nodeLabel = new NodeLabelLayout( NODE_DECORATORS );

        // create the filtering and layout
        ActionList filter = new ActionList();
        filter.add( new TreeRootAction( TREE, nodePositionLock, listModel,
                attrMapping ) );
        filter.add( fonts );
        filter.add( treeLayout );
        // filter.add( edgeLabel );
        // filter.add( nodeLabel );
        filter.add( subLayout );
        filter.add( textColor );
        filter.add( nodeColor );
        filter.add( edgeColor );
        filter.add( edgeSize );
        m_vis.putAction( "filter", filter );

        // animated transition
        ActionList animate = new ActionList( 1250 );
        animate.setPacingFunction( new SlowInSlowOutPacer() );
        animate.add( new QualityControlAnimator() );
        animate.add( new VisibilityAnimator( TREE ) );
        animate.add( new PolarLocationAnimator( NODES, linear ) );
        animate.add( new ColorAnimator( NODES ) );
        animate.add( new RepaintAction() );
        m_vis.putAction( "animate", animate );
        // m_vis.alwaysRunAfter( "filter", "animate" );

        // lock nodes
        ActionList lock = new ActionList();
        SetLockNodeAction lockNode = new SetLockNodeAction( nodePositionLock );
        lock.add( lockNode );
        m_vis.putAction( "lock", lock );

        // filter and hide
        // XXX: Not used yet
        ActionList filterAndHide = new ActionList();
        final GraphDistanceFilter level1Selected = new GraphDistanceFilter(
                TREE, Visualization.FOCUS_ITEMS, 1 );
        filterAndHide.add( level1Selected );
        filterAndHide.add( filter );
        m_vis.putAction( "filterAndHide", filterAndHide );

        // show edges returned from results
        ActionList showEdgeResults = new ActionList();
        final GraphDistanceFilter level0 = new GraphDistanceFilter( TREE,
                Visualization.SEARCH_ITEMS, 0 );
        // showEdgeResults.add( level0 );
        final ShowSearchResultEdgeAction edges = new ShowSearchResultEdgeAction();
        showEdgeResults.add( edges );
        m_vis.putAction( "show_edge_results", showEdgeResults );

        // show nodes returned from results
        ActionList showNodeResults = new ActionList();
        final GraphDistanceFilter level1 = new GraphDistanceFilter( TREE,
                Visualization.SEARCH_ITEMS, 1 );
        showNodeResults.add( level1 );
        m_vis.putAction( "show_node_results", showNodeResults );

        // restore all nodes/edges
        ActionList unhide = new ActionList();
        // distance between each node
        final GraphDistanceFilter levelInf = new GraphDistanceFilter( TREE,
                TREE, 5000 );
        unhide.add( levelInf );
        m_vis.putAction( "unhide", unhide );

        ActionList fileOpen = new ActionList();
        // don't add this if it's standalone
        if( !STAND_ALONE ) {
            fileOpen.add( new FileOpenAction( TREE ) );
        }
        fileOpen.add( filter );
        m_vis.putAction( "file_open", fileOpen );

        // XXX: highlight nodes based on selected edge
        // ActionList edgeFocus = new ActionList();
        // NodeOrderAction nodeOrder = new NodeOrderAction( EDGES );
        // edgeFocus.add( nodeOrder );
        // m_vis.putAction( "edgeFocus", edgeFocus );

        // ------------------------------------------------

        // initialize the display
        setSize( width, height );
        setItemSorter( new TreeDepthItemSorter() );
        addControlListener( new DragControl() );

        // right-mouse-click zoom-to-fit
        addControlListener( new ZoomToFitControl() );

        // left-mouse-click zoom on selected on-focus node
        // XXX: Uncomment later, important
        // addControlListener( new ZoomToFitControl( Visualization.FOCUS_ITEMS,
        // 150, 0, Control.LEFT_MOUSE_BUTTON ) );

        addControlListener( new ZoomControl() );
        addControlListener( new PanControl() );
        addControlListener( new NeighborHighlightControl() );

        // focus control applies to only nodes
        FocusControl focusControl = new FocusControl( 1, "filter" );
        focusControl.setFilter( new InGroupPredicate( NODES ) );
        addControlListener( focusControl );

        // XXX: double click for opening files
        FocusControl fileControl = new FocusControl( 2, "file_open" );
        fileControl.setFilter( new InGroupPredicate( NODES ) );
        addControlListener( fileControl );

        // XXX: highlight nodes based on edges
        // FocusControl edgeFocusControl = new FocusControl( 0, "edgeFocus" );
        // edgeFocusControl.setFilter( new InGroupPredicate( EDGES ) );
        // addControlListener( edgeFocusControl );

        // XXX: Uncomment later
        // use filter and hide to show directly connected node
        // addControlListener( new FocusControl( 1, "filterAndHide" ) );

        addControlListener( new HoverActionControl( "repaint" ) );
        addControlListener( new ToolTipControl( "name" ) );

        // ------------------------------------------------

        // filter graph and perform layout
        m_vis.run( "filter" );

        // maintain a set of items that should be interpolated linearly
        // this isn't absolutely necessary, but makes the animations nicer
        // the PolarLocationAnimator should read this set and act accordingly
        m_vis.addFocusGroup( linear, new DefaultTupleSet() );
        m_vis.addFocusGroup( BLUE_EDGE_SET, new DefaultTupleSet() );
        m_vis.getGroup( Visualization.FOCUS_ITEMS ).addTupleSetListener(
                new TupleSetListener() {
                    public void tupleSetChanged( TupleSet t, Tuple[] add,
                                                 Tuple[] rem ) {
                        TupleSet linearInterp = m_vis.getGroup( linear );
                        if( add.length < 1 ) {
                            return;
                        }
                        linearInterp.clear();
                        Node n = ( add[0] instanceof Node ) ? ( Node ) add[0]
                                : null;
                        for(; n != null; n = n.getParent() ) {
                            linearInterp.addTuple( n );
                        }
                    }
                } );

        SearchTupleSet search = new PrefixSearchTupleSet( false );
        // add search_items
        m_vis.addFocusGroup( Visualization.SEARCH_ITEMS, search );
        search.addTupleSetListener( new TupleSetListener() {
            public void tupleSetChanged( TupleSet t, Tuple[] add, Tuple[] rem ) {
                m_vis.cancel( "animatePaint" );
                // update tree nodes here...
                if( t == null || t.getTupleCount() == 0 ) {
                    m_vis.run( "unhide" );
                }
                else {
                    m_vis.run( "show_node_results" );
                    m_vis.run( "show_edge_results" );
                }
                m_vis.run( "recolor" );
                m_vis.run( "animatePaint" );
                // this doesn't work right....
                // zoomToFit();
                // m_vis.run( "filter" );
            }
        } );
    }

    /**
     * this starts at 0...
     *
     * @param g graph
     */
    private void createNameIdMapper( Graph g ) {
        for( int i = 0; i < g.getNodeCount(); i++ ) {
            Node n = g.getNode( i );
            nameIdMapper.put( getUpperLetterOnly( n.getString( "name" ) ), i );
        }
        int i = 0;
        i++;
    }

    private static String getUpperLetterOnly( String input ) {
        StringBuffer sb = new StringBuffer();
        input = input.toUpperCase();
        char c = 0;
        for( int i = 0; i < input.length(); i++ ) {
            c = input.charAt( i );
            if( Character.isLetter( c ) ) {
                sb.append( c );
            }
        }
        return sb.toString();
    }

    public void resizeAndRun( int width, int height ) {
        zoomToFit();
        setSize( width, height );
        m_vis.run( "recolor" );
        m_vis.run( "animatePaint" );
    }

    public void externalSetFocus( String nodeName ) {
        nodeName = sanitizeNodeName( nodeName );
        Graph g = ( Graph ) m_vis.getGroup( TREE );
        Node f = getNodeByName( g, nodeName );
        if( f != null && g.containsTuple( f ) ) {
            m_vis.getGroup( Visualization.FOCUS_ITEMS ).setTuple( f );
            // run filter after setting focus item, this is the same as a
            // "focus action"
            m_vis.run( "filter" );
        }
    }

    public void highLightEdgeByAttribute( String attribute ) {
        if( !attrMapping.containsKey( attribute ) ) {
            return;
        }

        TupleSet edgeSet = m_vis.getGroup( VizDemoRadial.BLUE_EDGE_SET );
        edgeSet.clear();

        // XXX: highlight or blue edge set?

        // unhighlight previously highlighted items
        for( EdgeItem edge : prevHighlightedEdges ) {
            edge.setHighlighted( false );
        }

        // now set the highlight we want to true
        for( EdgeItem edge : attrMapping.get( attribute ) ) {
            edge.setHighlighted( true );
        }

        prevHighlightedEdges = attrMapping.get( attribute );

        // blue edge set
        // for( EdgeItem edge : attrMapping.get( attribute ) ) {
        // edgeSet.addTuple( edge );
        // }

        // repaint the visualization
        m_vis.run( "repaint" );
    }

    private static String sanitizeNodeName( String nodeName ) {
        String name = nodeName.split( "\\." )[0];
        name = name.replaceAll( ".nterview.*", "" );
        name = name.replaceAll( "Cor.*port.*", "" );
        name = name.replaceAll( "Pol.*port.*", "" );
        return name;
    }

    private void zoomToFit() {
        long duration = 10;
        int margin = 5;

        Visualization vis = getVisualization();
        Rectangle2D bounds = vis.getBounds( TREE );
        GraphicsLib.expand( bounds, margin + ( int ) ( 1 / getScale() ) );
        DisplayLib.fitViewToBounds( this, bounds, duration );
    }

    // ----------------------------------------------------------------
    // ----------------------------Factories---------------------------
    // ----------------------------------------------------------------

    public static JPanel demo() {
        return demo( DATA_FILE, "name", 600, 600 );
    }

    public static JPanel demo( String datafile, final String label,
                               final int width, final int height ) {
        Graph g = null;
        try {
            g = new GraphMLReader().readGraph( datafile );
        } catch( Exception e ) {
            e.printStackTrace();
            System.exit( 1 );
        }
        return demo( g, label, width, height );
    }

    public static JPanel demo( Graph g, final String label, final int width,
                               final int height ) {
        // create a new radial tree view
        final VizDemoRadial gview = new VizDemoRadial( g, label, width,
                height - 30 );
        Visualization vis = gview.getVisualization();

        // search within nodes
        // SearchQueryBinding sq = new SearchQueryBinding(
        // (Table) vis.getGroup( NODES ), label,
        // (SearchTupleSet) vis.getGroup( Visualization.SEARCH_ITEMS ) );

        SearchQueryBinding sq = new SearchQueryBinding( vis.getGroup( TREE ),
                "name",
                ( SearchTupleSet ) vis.getGroup( Visualization.SEARCH_ITEMS ) );

        JSearchPanel search = sq.createSearchPanel();
        search.setShowResultCount( true );
        search.setBorder( BorderFactory.createEmptyBorder( 5, 5, 4, 0 ) );
        search.setFont( FontLib.getFont( "Tahoma", Font.PLAIN, 11 ) );

        final JFastLabel title = new JFastLabel( "Attributes" );
        title.setPreferredSize( new Dimension( 350, 20 ) );
        title.setVerticalAlignment( SwingConstants.BOTTOM );
        title.setBorder( BorderFactory.createEmptyBorder( 3, 0, 0, 0 ) );
        title.setFont( FontLib.getFont( "Tahoma", Font.PLAIN, 16 ) );

        final Box box = new Box( BoxLayout.X_AXIS );
        box.add( Box.createHorizontalStrut( 10 ) );
        box.add( title );
        box.add( Box.createHorizontalGlue() );
        box.add( search );
        box.add( Box.createHorizontalStrut( 3 ) );

//        final JList list = new JList( listModel );
//        list.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
//        list.setSelectedIndex( -1 );
//        list.addListSelectionListener( new ListSelectionListener() {
//            public void valueChanged( ListSelectionEvent e ) {
//                if( list.getSelectedIndex() == -1
//                        || list.getSelectedValue() == null
//                        || list.getSelectedValue().toString().trim().isEmpty() ) {
//                    return;
//                }
//                gview.highLightEdgeByAttribute( list.getSelectedValue()
//                        .toString() );
//            }
//        } );

//        final JScrollPane listScrollPane = new JScrollPane( list );
//        listScrollPane.setVisible( false );

        final JPanel panel = new JPanel( new BorderLayout() );
        panel.add( gview, BorderLayout.CENTER );
//        panel.add( listScrollPane, BorderLayout.WEST );
        panel.add( box, BorderLayout.SOUTH );

        Color BACKGROUND = Color.WHITE;
        Color FOREGROUND = Color.DARK_GRAY;
        UILib.setColor( panel, BACKGROUND, FOREGROUND );

        gview.addControlListener( new ControlAdapter() {
            public void itemEntered( VisualItem item, MouseEvent e ) {
                // if this is an edge
                if( item instanceof EdgeItem ) {
                    title.setText( "Attributes: " + item.getString( "name" ) );
                    return;
                }
                if( item.canGetString( label ) ) {
                    title.setText( item.getString( label ) );
                }
            }

            public void itemExited( VisualItem item, MouseEvent e ) {
                title.setText( null );
            }

//            public void itemClicked( VisualItem visualItem, MouseEvent mouseEvent ) {
//                if( visualItem instanceof NodeItem ) {
//                    if( !listScrollPane.isVisible() ) {
//                        listScrollPane.setVisible( true );
//                        // this resizes the pane
//                        panel.revalidate();
//                        panel.repaint();
//                    }
//                }
//            }
        } );

        return panel;
    }

    //
    //
    // ----------------------------------------------------------------
    // ---------------------- MAIN RUNNER -----------------------------
    // ----------------------------------------------------------------
    //
    //
    public static void main( String argv[] ) {
        STAND_ALONE = true;

        String infile = DATA_FILE;
        String label = "name";

        if( argv.length > 1 ) {
            infile = argv[0];
            label = argv[1];
        }

        UILib.setPlatformLookAndFeel();

        JFrame frame = new JFrame(
                "i n f o v i z | r a d i a l v i e w - d e m o" );
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setContentPane( demo( infile, label, 600, 600 ) );
        frame.pack();
        frame.setVisible( true );
    }

} // end of class RadialGraphView